#HashiTalks Japan 2024 で「TerraformでEKSを操る実践的Tipsと効率化テクニック」という登壇をしました
HashiTalks: Japanにて、「TerraformでEKSを操る実践的Tipsと効率化テクニック」というタイトルの発表をいたしました。ご視聴いただいたみなさま、またこのような機会をくださったHashicorpのみなさま、ありがとうございました!
本エントリはその発表内容をブログ用に再編したものになります。
TerraformでEKSを操る実践的Tipsと効率化テクニック
私は現在参画中のプロジェクトで約4年間EKSを使用しており、IaCツールとしてTerraformも採用しています。
4年の経験でEKSワークロードのIaCツールとしてTerraformを使う際の知見が色々貯まってきたので、今回共有したいと思います。
今回お伝えしたいこと
お伝えしたいことは以下の3つになります。
- applyを分ける
- 公開モジュールを最大限活用する
- EKS Blueprintsを参考、利用する
さっそく1つ目の「1. applyを分ける」からご説明いたします。
1. applyを分ける
もう少し具体的にしますと「EKSクラスターのプロビジョニングと、そのクラスター内のkubernetes リソースをデプロイする terraform apply
コマンドは分けるべき」ということです。
前提: Terraformは色々なリソースのプロビジョニングに使える
まず前提として、Terraformは色々なリソースのプロビジョニングに使えるツールです。AWS providerを使ってEKSクラスターをプロビジョニングすることができます。
さらに、Kubernetes,helm,kubectl providerなどを使って、そのEKSクラスター上のkubernetesリソースをデプロイすることもできます。DeploymentsとかServiceとかCRDとかですね。
これら、EKSクラスターと、その上のkubernetesリソースを同じTerraformのルートモジュール上で管理するようにして、terraform apply
コマンド一発で全リソース作成することもできますが、それはやらないほうがいいですよ、という話です。
理由は2つあります。
理由1: ライフサイクルが違うから
ひとつめの理由はライフサイクルが違うから、です。
EKSクラスターは、アプリを稼働させるプラットフォームであり、インフラリソースであると言えます。
一方 kubernetesリソースは、アプリそのものとより密接に関わっています。
この性質の違いにより、お互いのライフサイクル、更新頻度も通常異なってきます。そういったものを同じTerraform root module下で管理するべきではない、という意見です。
理由2: 不安定だから
2つ目の理由は不安定だから、です。こちらを今回より深く取り上げたいと思っています。
- Kubernetes、Helm、kubectl providerが作ろうとする各種kubernetesリソースの作成は、クラスターのコントロールプレーンに存在する APIサーバーにリクエストすることをトリガに実行される処理です。
- そのためKubernetes、Helm、kubectl providerの設定には、そのAPIサーバーのエンドポイント を指すhost attribute が必要になります。
- しかしこのAPIサーバーのエンドポイントはクラスターの一部なので、クラスターが作成完了してからでないとアクセスできません。
- というわけでaws_eks_clusterリソースからエクスポートされるエンドポイントアドレスが必要です。以下が例です。host以外のattributeの値も、
aws_eks_cluster
リソース(または、クラスターが存在することを前提とするdata sourcedata.aws_eks_cluster_auth
)のattribute値を参照しています。
provider "kubernetes" {
host = aws_eks_cluster.this.endpoint
cluster_ca_certificate = base64decode(aws_eks_cluster.this.certificate_authority[0].data)
token = data.aws_eks_cluster_auth.this.token
}
- つまり、これらのプロバイダの設定は、
aws_eks_cluster
リソースに依存しているということになります。
で、結局これの何がマズイのか、ですが…
色々と問題が発生します。クラスターがすでにある状態でのapplyは問題ないのですが、クラスターを新たに作成するときや、逆にクラスターを削除したいときに問題が発生しやすいです。
-
特定のリソース(具体的には
kubernetes_manifest
)においてはplanフェーズでAPIサーバーにアクセスしに行くので、この段階でクラスターが作成済でないとエラーになります。 -
planは通るけどapplyフェーズでエラーになる、といったケースも発生し得ます。
- エンドポイントにはIPによるアクセス制限を設定することができます。applyフェーズにてクラスターが作成され、エンドポイントにIP制限がかけられたがためにその後kubernetesリソースが作成できない、とうエラーが発生する場合があります。
- Terraformコマンドを実行しているIAMエンティティ(IAMユーザーやIAMロールのこと)が実はkubernetesリソースをデプロイできる権限を付与されておらずエラーになる、という可能性もあります。
- ※ 以前は、クラスターを作成したIAMエンティティには暗黙的にクラスター内のフルアクセス権限が与えられていたのでこの問題が発生することはありませんでした。が、この「暗黙的にフルアクセス権限が与えられる」がセキュリティ的に好ましくありませんし、かつわかりにくいということで、オプションで与えないようにできるサービスアップデートがありました。
-
また、クラスター削除は以下のように3段階にわけてapplyするべきです。こうしないと
Reference to undeclared resource
エラーになったり、terraform planだけでk8sリソースがTerraform管理外になったりします。(なお、terraform destroy
だと1発でいけたりします…🤔)- まず k8sリソースを全て削除、もしくは Terraform管理外にする
- その後 kubernetes|helm|kubectl provider blockを削除
- 最後にクラスター削除
こんな感じで、総じて不安定であると言えるかなと思います。
公式ドキュメントでも言及されている
実は公式ドキュメントでもこの問題は言及されています。こちらは以下のkubernetes provider のドキュメントをChromeで日本語訳したものです。
赤枠で囲ったところ抜粋します。
他のリソースから Kubernetes プロバイダーに資格情報を渡す場合、これらのリソースは、Kubernetes プロバイダー リソースも使用されている同じ Terraform モジュールで作成しないでください。これにより、デバッグや診断が困難な断続的で予測できないエラーが発生します。
Kubernetes プロバイダーを構成する最も信頼性の高い方法は、クラスター自体と Kubernetes プロバイダーのリソースを別々の
apply
操作で管理できるようにすることです。
また、kubernetes providerではなく、Terraform本体のproviderの設定の仕方についてのドキュメントにも記載があります。
You can use expressions in the values of these configuration arguments, but can only reference values that are known before the configuration is applied. This means you can safely reference input variables, but not attributes exported by resources (with an exception for resource arguments that are specified directly in the configuration).
(訳) providerの設定引数の値に式を使用できますが、設定が適用される前に既知の値のみを参照できます。つまり、入力変数は安全に参照できますが、リソースによってエクスポートされた属性は参照できません(構成で直接指定されたリソース引数を除く)。
このドキュメントに則ると、先程お見せした以下 provider設定は「できない」ということになると思います。できないのに実質できてしまっているという状態なので先程のような不安定さが発生するのは致し方ないかなと私は解釈しています。
provider "kubernetes" {
host = aws_eks_cluster.this.endpoint
cluster_ca_certificate = base64decode(aws_eks_cluster.this.certificate_authority[0].data)
token = data.aws_eks_cluster_auth.this.token
}
続いて、この問題の対処方法をご説明します。方針としては2つあります。
対処方法1: ルートモジュールを分ける
ひとつめはシンプルにルートモジュールを分けるという方法です。
kubernetes providerの実装例でもこの方法が採られていました。
まずeks clusterディレクトリにてapplyしてクラスターを作成し、続いてkubernetes-configディレクトリに移動してapplyし、k8sリソースを作成します。
この方法の欠点はapplyを2回実行しないといけない点です。CDパイプラインも2つ用意する必要があるでしょう。
なのですが、新機能Terraform Stacksがこの欠点を解消してくれそうです。Terraform Stackは現在 HCP Terraformでのみ使えるパブリックベータの機能で、複数のRootモジュール、HCP Terraformでいうところのworkspaceの依存関係を定義し、apply実行順を制御したり、workspace間で値を受け渡したりできます。
これを使えば workspaceを分けつつ、パイプラインは一つで済ます、ということが実現できそうです。こちらはまた試してみたいと思います。
Terraform Stacksについての詳細は、本日のHashiTalksで弊社の佐藤雅樹さんが解説してくれてますのでこちらをチェックください!
対処方法2: targetオプションでapplyを分ける
話戻りまして、applyを分ける 2つ目の方法は「targetオプションを使う」です。
terraform apply -auto-approve -target="aws_eks_cluster.this"
terraform apply -auto-approve
クラスターを作成するapply時のみ上記のようにapplyを分けます。
まず、1行目、target optionを使ってクラスター作成までを初回のapplyの対象とします。
次に2回目のapplyにて kubernetes リソース作成部分を対象に含めます。
こうすることで、kubernetesリソースを作成する際にはクラスターが作成済の状況を実現できますので、問題が発生しなくなります。
プロジェクトではtargetオプションを採用
現在のプロジェクトでは2のtargetオプションでapplyを分ける方法を採っています。その理由ですが、 AWSリソースと一部k8sリソースを併せてデプロイ、管理したいからです。
まず、併せて管理する必要性のない、アプリを直接構成するk8sリソースについては、Terraform外で管理しています。CDパイプラインも別になっています。
それとは別に、インフラリソース的に扱いたいk8sリソースがいくつかあり、これらはTerraform内でデプロイするようにしています。具体的には Prometheus、Fluent-Bit、ArgoCDなどです。
また、AWSリソース と k8sリソースで1コンポーネントになるものも、Terraoformでまとめて管理するようにしています。
具体的にはまず IRSAです。これはPod単位でIAM ロールが使える、つまりAWS権限設定をきめ細やかに設定することのできる機能なのですが、IAMロールやポリシーと行ったAWSリソースに加えて、サービスアカウントというkubernetes リソースも必要です。
このサービスアカウントの設定にIAM RoleのARNが必要で、このサービスロールの作成をTerraform外でやるとARNの引き渡しが面倒なので、Terraformでまとめています。
同様にAWS LoadBalancer Controller、これはkubernetesマニフェストでALB等のAWSのロードバランサー系のサービスが管理できる機能ですが、これを構成するkubernetesリソースにVPC idを渡す必要があり、先程のIRSAと同様利便性の観点でTerraformで管理しています。
またIRSAもAWS LoadBalancer Controllerも、アプリを動かす前提となる機能なので、先程ご説明した「インフラリソースとして扱いたい」という点にも合致しています。
以上でtips1. applyを分けるの説明を終わります。続いて ふたつめ「公開モジュールを最大限活用する」に進みます。
2. Published modulesを最大限活用する
課題: たくさんのリソースを作る必要があって大変
EKS周りで必要になるAWSリソースは多岐にわたり、その構成は複雑です。
EKSクラスターに加えて、EC2ノードを使うならノードグループ、Fargateを使うなら Fargate Profileの定義が必要です。IAM系のリソースも色々と必要ですし、セキュリティグループとそのルールの設定も複雑です。
そのTerraformコードを全て自分書くことは大変で時間がかかります。
terraform-aws-eks モジュールを使って楽しよう
そこで代わりに Terraform Registryにある terraform-aws-eks moduleを利用することを提案します。
この moduleを使うことで、Terraformコードを書く時間を大幅に減らすことができます。
例
https://github.com/aws-ia/terraform-aws-eks-blueprints/blob/main/patterns/aws-vpc-cni-network-policy/main.tf こちらでこのmoduleが以下のように使われています。コメント含めわずか27行のコードです。
module "eks" {
source = "terraform-aws-modules/eks/aws"
version = "~> 20.11" # 現時点の最新版 20.28.0を使用
cluster_name = local.name
cluster_version = "1.30" # Must be 1.25 or higher
cluster_endpoint_public_access = true
# Give the Terraform identity admin access to the cluster
# which will allow resources to be deployed into the cluster
enable_cluster_creator_admin_permissions = true
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnets
eks_managed_node_groups = {
initial = {
instance_types = ["m5.large"]
min_size = 3
max_size = 10
desired_size = 5
}
}
tags = local.tags
}
この短いコードをapplyすると、計46個のリソース&データソースが作成されました。このような感じで、細かい実装をショートカットして要件を実現できるので、この moduleはとてもおすすめです。
% terraform state list | grep module.eks
module.eks.data.aws_caller_identity.current
module.eks.data.aws_iam_policy_document.assume_role_policy[0]
module.eks.data.aws_iam_session_context.current
module.eks.data.aws_partition.current
module.eks.data.tls_certificate.this[0]
module.eks.aws_cloudwatch_log_group.this[0]
module.eks.aws_ec2_tag.cluster_primary_security_group["Blueprint"]
module.eks.aws_ec2_tag.cluster_primary_security_group["GithubRepo"]
module.eks.aws_eks_access_entry.this["cluster_creator"]
module.eks.aws_eks_access_policy_association.this["cluster_creator_admin"]
module.eks.aws_eks_cluster.this[0]
module.eks.aws_iam_openid_connect_provider.oidc_provider[0]
module.eks.aws_iam_policy.cluster_encryption[0]
module.eks.aws_iam_role.this[0]
module.eks.aws_iam_role_policy_attachment.cluster_encryption[0]
module.eks.aws_iam_role_policy_attachment.this["AmazonEKSClusterPolicy"]
module.eks.aws_iam_role_policy_attachment.this["AmazonEKSVPCResourceController"]
module.eks.aws_security_group.cluster[0]
module.eks.aws_security_group.node[0]
module.eks.aws_security_group_rule.cluster["ingress_nodes_443"]
module.eks.aws_security_group_rule.node["egress_all"]
module.eks.aws_security_group_rule.node["ingress_cluster_443"]
module.eks.aws_security_group_rule.node["ingress_cluster_4443_webhook"]
module.eks.aws_security_group_rule.node["ingress_cluster_6443_webhook"]
module.eks.aws_security_group_rule.node["ingress_cluster_8443_webhook"]
module.eks.aws_security_group_rule.node["ingress_cluster_9443_webhook"]
module.eks.aws_security_group_rule.node["ingress_cluster_kubelet"]
module.eks.aws_security_group_rule.node["ingress_nodes_ephemeral"]
module.eks.aws_security_group_rule.node["ingress_self_coredns_tcp"]
module.eks.aws_security_group_rule.node["ingress_self_coredns_udp"]
module.eks.time_sleep.this[0]
module.eks.module.eks_managed_node_group["initial"].data.aws_caller_identity.current
module.eks.module.eks_managed_node_group["initial"].data.aws_iam_policy_document.assume_role_policy[0]
module.eks.module.eks_managed_node_group["initial"].data.aws_partition.current
module.eks.module.eks_managed_node_group["initial"].aws_eks_node_group.this[0]
module.eks.module.eks_managed_node_group["initial"].aws_iam_role.this[0]
module.eks.module.eks_managed_node_group["initial"].aws_iam_role_policy_attachment.this["AmazonEC2ContainerRegistryReadOnly"]
module.eks.module.eks_managed_node_group["initial"].aws_iam_role_policy_attachment.this["AmazonEKSWorkerNodePolicy"]
module.eks.module.eks_managed_node_group["initial"].aws_iam_role_policy_attachment.this["AmazonEKS_CNI_Policy"]
module.eks.module.eks_managed_node_group["initial"].aws_launch_template.this[0]
module.eks.module.kms.data.aws_caller_identity.current[0]
module.eks.module.kms.data.aws_iam_policy_document.this[0]
module.eks.module.kms.data.aws_partition.current[0]
module.eks.module.kms.aws_kms_alias.this["cluster"]
module.eks.module.kms.aws_kms_key.this[0]
module.eks.module.eks_managed_node_group["initial"].module.user_data.null_resource.validate_cluster_service_cidr
他の公開moduleも使おう
まずVPCです。EKSのワーカーノードはVPCのサブネット上に配置しますので、VPCリソースは必須です。ですがVPC周りのリソースも結構色々な種類のリソースを定義しないといけなくて面倒です。ですのでこのモジュールを使って面倒な部分をショートカットしています。
また先程ご紹介したIRSA、これで使うIAM Roleも、IAMのモジュールのサブモジュールを使って作成しています。
3. EKS Blueprintsを参考、利用する
EKS Blueprints for Terraformとは
GitHubで公開されているEKSの実装例集です。
リポシトリーオーナーは aws-ia (Integration and Automation)ということで、AWSの中の人がメンテナンスしているリポジトリのようです。
ドキュメントページもありますので内容理解もしやすいです。
実装例
載っている実装例のほんの一部です。
- Fargate nodeのみの構成
- 完全 プライベートクラスター
- Blue / Green upgrade
- GitOps
- Karpenter
- IAM Identity Center Single Sign-On for Amazon EKS Cluster
EKSでやりたいことがあったらまずはこのリポジトリを覗いてみて、参考にする、またはコピペから始めるというのが手っ取り早い開発の仕方かなと思うので、一度チェックしてみてください!
詳細ブログもどうぞ
EKS Blueprintsについては詳細をブログ化しておりますので、気になる方はこちらもご確認ください。
まとめ
以上です。最後に本ブログでお伝えしたことをもう一度まとめます。
1つ目に、クラスターのプロビジョニングと、その上のk8sリソースのデプロイはapplyを分けるべきという話をしました。
2つ目、eks moduleをはじめとした各種公開moduleのご紹介で、
3つ目は、EKS Blueprintsという超有用な実装例集があるのでこれも参考、利用する、という話でした。
いつかどこかで今日お話した内容がお役に立てば幸いでございます。ありがとうございました。